<?php

class po
{
	public function __construct( )
	{
		$this ->setPluralForms( );
	}
	
	public function __get( $k )
	{
		switch( $k )
		{
		case 'headers':
		case 'entries':
			return $this ->_parsed[ $k ];
		case 'all':
			return $this ->_parsed;
		case 'plural':
			return $this ->_inner['plural'];
		case 'number':
		case 'forms':
			return $this ->_inner['plural'][$k];
		}
		return null;
	}
	
	protected $_inner = array( );
	protected $_parsed = array( );
	
	/**
	 * {
	 * 'translator_comments'   => '',
	 * 'extracted_comments': '',
	 * 'references': [],
	 * 'flags': [ 'fuzzy', 'php-format', ... ],
	 * 'previous_context': '',
	 * 'previous_tag': '',
	 * 'previous_plural': '',
	 * 
	 * 'context': '',
	 * 'tag': '',
	 * 'plural': '',
	 * 'value': [] // 1 or many plural forms
	 * }
	 *
	 * @see http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
	 */
	
	// fixme: decode?
	public function unquote( $s )
	{
		$s = stripcslashes( $s );
		return ($s[0]=='"' && substr($s,-1,1)=='"') ? substr( $s, 1, -1 ) : $s;
	}
	
	// fixme: normalize
	public function quote( $a )
	{
		$s = addcslashes( $a, "\0..\37\"\\" );
		if( strpos( $a, "\n" ) !== false && substr( $a, -1, 1 ) != "\n" )
			$s = "\"\n\"".str_replace( '\n', "\\n\"\n\"", $s );
		return sprintf( '"%s"', $s );
	}
	
	/**
	 * Parsers
	 *
	 */
	public function load( $file ) { $this ->parseFile( $file ); }
	public function parseFile( $filename, $mode = null )
	{
		// basic file verification — бляяяя
		if( !is_file($filename) )
			throw new Exception('The specified file does not exist.');
		
		//if( substr($filename, strrpos($filename, '.')) !== '.po' )
		//	throw new Exception('The specified file is not a PO-file.');
		
		return $this ->parse( file_get_contents( $filename ), $mode );
	}
	
	public function parse( $data, $mode = null )
	{
		$this ->setPluralForms( );
		
		$blocks = explode( "\n\n", str_replace( "\r\n", "\n", $data ) );
		$headers = $this ->parseHead( array_shift( $blocks ) );
		
		// set plurals
		if( isset( $headers['Plural-Forms'] ) )
			$this ->setPluralForms( $headers['Plural-Forms'] );
		
		$entries = array( );
		foreach( $blocks as $block )
			$entries[] = $this ->parseEntry( $block );
		$entries = array_filter( $entries );
		
		if( empty( $headers ) && empty( $entries ) ) return null;
		
		$this ->_parsed = array( 'headers' => $headers, 'entries' => $entries );
		
		switch( $mode )
		{
		case 'headers':
		case 'entries':
			return $this ->_parsed[ $mode ];
		case 'all':
			return $this ->_parsed;
		}
		return null;
	}
	
	
	protected $headers = array(
		'Project-Id-Version'		=> 'PACKAGE VERSION',
		'Report-Msgid-Bugs-To'		=> '',
		'POT-Creation-Date'		=> '',
		'PO-Revision-Date'		=> 'YEAR-MO-DA HO:MI+ZONE',
		'Last-Translator'		=> 'FULL NAME <EMAIL@ADDRESS>',
		'Language-Team'			=> 'LANGUAGE <LL@li.org>',
		'MIME-Version'			=> '1.0',
		'Content-Type'			=> 'text/plain; charset=CHARSET',
		'Content-Transfer-Encoding'	=> '8bit',
		'Plural-Forms'			=> 'nplurals=INTEGER; plural=EXPRESSION;',
		);
	
	protected function parseHead( $s )
	{
		$r = array( );
		foreach( explode( "\n", $s ) as $line )
		if( trim($line) )
		{
			$line = $this ->unquote( $line );
			$ci = strpos( $line, ':' ); // colon index
			if( $ci === false ) continue;
			
			$k = trim(substr( $line, 0, $ci )); // header name
			$v = trim(substr( $line, $ci + 1, -1 )); // header value
			if( !isset( $this ->headers[ $k ] ) || !strlen($v) || $this ->headers[ $k ] == $v ) continue;
			
			$r[ $k ] = $v;
		}
		
		return $r;
	}
	
	protected function renderHead( $headers )
	{
		$r = "# autogenerated\n#\n";
		$r .= "#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n";
		
		$defaults = array(
			'Project-Id-Version' => 'XMS 0.1',
			'POT-Creation-Date' => date( 'Y-m-d H:iO' ),
			'PO-Revision-Date' => date( 'Y-m-d H:iO' ),
			'Content-Type' => 'text/plain; charset=UTF-8',
			'Plural-Forms' => 'nplurals=2; plural=n!=1;',
			);
		
		foreach( $defaults as $k => $v )
		if( !isset( $headers[ $k ] ) )
			$headers[ $k ] = $v;
		
		foreach( $this ->headers as $k => $v )
		{
			if( isset( $headers[ $k ] ) ) $v = $headers[ $k ];
			$r.= $this ->quote( sprintf( "%s: %s\n", $k, $v ) )."\n";
		}
		return $r;
	}
	
	protected $specialComments = array(
			' '	=> 'translator_comments',
			'.'	=> 'extracted_comments',
			':'	=> 'references',
			','	=> 'flags',
			'|'	=> 'previous',
			'| msgctxt'	=> 'previous_context',
			'| msgid'	=> 'previous_tag',
			'| msgid_plural'=> 'previous_plural',
			);
	
	protected function parseEntry( $s )
	{
		$s = str_replace( "\"\n\"", '', $s );
		$lines = explode( "\n", $s );
		
		$r = array( );
		$kv = $this ->specialComments;
		foreach( $lines as $l )
		{
		if( !isset( $l[0] ) ) continue;
		
		if( $l[0] == '#' )
		{
			$k = trim( $l[1] );
			if( !isset( $kv[$k] ) ) $k = ' ';
			
			// #| msgctxt previous-context
			// #| msgid previous-tag
			// #| msgid_plural previous-plural
			if( $k == '|' )
			{
				$sp = strpos( $l, ' ', 3 ) - 1;
				$sk = substr( $l, 1, $sp );
				$sv = substr( $l, $sp + 1 );
				$r[ $kv[$sk] ] = $this ->unquote( $sv );
				continue;
			}
			$c = ltrim( substr( $l, 2 ) );
			
			$string = (trim( $k, ' .' ) == '');
			if( !isset( $r[$kv[$k]] ) ) $r[$kv[$k]] = $string ? '' : array( );
			if( $string ) $r[$kv[$k]] .= $c."\n"; else $r[$kv[$k]][] = $c;
		}
		elseif( strpos($l, 'msgctxt') === 0 )
		{
			$r['context'] = $this->unquote( substr($l, 8) );
		}
		elseif( strpos($l, 'msgid') === 0 )
		{
			if ($l[5] === ' ') {
				$r['tag'] = $this->unquote(substr($l, 6));
			}
			// msgid[_]plural
			else {
				$r['plural'] = $this->unquote(substr($l, 13));
			}
		}
		else if (strpos($l, 'msgstr') === 0) {
			// no plural forms
			if ($l[6] === ' ') {
				$r['value'] = $this->unquote(substr($l, 7));
			}
			// plural forms
			else {
				if( !isset( $r['value'] ) )
					$r['value'] = array( );
				
				$r['value'] = (array)$r['value'];
				$r['value'][] = $this->unquote( substr($l, strpos($l, ' ') + 1) );
			}
		}
		else if( $l[0] === '"' && isset( $r['value'] ) ) {
			$l = "\n" . preg_replace( '/([^\\\\])\\\\n$/', "\$1\n", $this->unquote($l) );
			if( !is_array($r['value']) )
				$r['value'] .= $l;
			else
				$r['value'][count($r['value'])-1] .= $l;
		}
		
		}
		
		
		if( !isset( $r['tag'] ) )
			return null;
		
		foreach( array_keys( $r ) as $k )
			if( is_string( $r[$k] ) ) $r[$k] = rtrim( $r[$k], "\r\n" );
		
		// normalize
		$r['value'] = (array)$r['value'];
		
		return $r;
	}
	
	private function renderkv( $k, $v )
	{
		return sprintf( "%s %s\n", $k, $this ->quote( $v ) );
	}
	
	protected function renderEntry( $entry )
	{
		$vk = array_flip( $this ->specialComments );
		
		$r = '';
		foreach( array_keys( $vk ) as $k )
		if( isset( $entry[$k] ) )
		{
			foreach( (array)($entry[$k]) as $c )
			if( mb_strlen($c) )
			$r .= sprintf( "#%s %s\n", $vk[$k], str_replace( "\n", sprintf( "\n#%s ", $vk[$k] ), $c ) );
			unset( $entry[$k] );
		}
		
		/* was earlier, but i think comments must be sorted as usual (or priority maybe?):
		foreach( $entry as $k => $v )
		if( isset( $vk[$k] ) )
		{
			foreach( (array)($v) as $c )
			$r .= sprintf( "#%s %s\n", $vk[$k], str_replace( "\n", sprintf( "\n#%s ", $vk[$k] ), $c ) );
			unset( $entry[$k] );
		} //*/

		
		// msgid
		if( isset( $entry['context'] ) )
			$r .= $this ->renderkv( 'msgctxt', $entry['context'] );
		
		// tag
		$r .= $this ->renderkv( 'msgid', $entry['tag'] );
		
		// plurals
		if( isset( $entry['plural'] ) && mb_strlen( $entry['plural'] )>0 )
		{
			$r .= $this ->renderkv( 'msgid_plural', $entry['plural'] );
			$count = $this ->getPluralForms( 'number' );
			$values = array_pad( (array)($entry['value']), $count, '' );
			foreach( $values as $k => $v )
				$r .= $this ->renderkv( 'msgstr['.$k.']', $v );
		}
		// simple
		else
			$r .= $this ->renderkv( 'msgstr', reset( $entry['value'] ) );
		
		return $r;
	}
	
	public function render( $entries, $headers = null, $language = null )
	{
		if( !is_null( $language ) )
		$headers += $this ->createLanguageHeaders( $language );
		
		// set plurals
		$this ->setPluralForms( $headers['Plural-Forms'] );
		
		$r = $this ->renderHead( $headers )."\n";
		foreach( $entries as $entry )
			$r .= $this ->renderEntry( $entry )."\n";
		
		return $r;
	}
	
	public function createLanguageHeaders( $language )
	{
		$r = array( );
		
		if( isset($language['translator']) && !is_null($language['translator'])
		&& strlen( $language['translator'] ) > 1 && strpos( $language['translator'], '@' ) !== false )
		$r['Last-Translator'] = str_replace( '"', '”', $language['translator'] );
		
		if( isset($language['team']) && !is_null($language['team']) )
		$r['Language-Team'] = str_replace( '"', '”', $language['team'] );
		
		if( !is_null($language['plural_number']) && !is_null($language['plural_forms']) )
		$r['Plural-Forms'] = str_replace(
			array( 'INTEGER', 'EXPRESSION' ),
			array( $language['plural_number'], $language['plural_forms'] ),
			$this ->headers['Plural-Forms'] );
		
		if( isset( $language['charset'] ) && !is_null( $language['charset'] ) )
		$r['Content-Type'] = str_replace( 'CHARSET', $language['charset'], $this ->headers['Content-Type'] );
		
		return $r;
	}
	
	
	
	public function setPluralForms( $pn = 1, $pf = '0' )
	{
		if( is_numeric( $pn ) && $pn < 7 && $pn > 0 && strlen( $pf )>0 )
		{
			$this ->_inner['plural']['number'] = $pn;
			$this ->_inner['plural']['forms'] = $pf;
			return;
		}
		
		preg_match( '@nplurals=(?P<number>\d); plural=(?P<forms>[^;]+);@i', $pn, $m );
		foreach( array( 'number', 'forms' ) as $k )
			$this ->_inner['plural'][$k] = $m[$k];
	}
	
	public function getPluralForms( $k )
	{
		if( !isset( $this ->_inner['plural'][$k] ) ) $k = "plural_".$k;
		if( !isset( $this ->_inner['plural'][$k] ) ) return null;
		return $this ->_inner['plural'][$k];
	}
	
	
	
	public function getPath( $lang, $domain = null, $root = null )
	{
		$category = 'LC_MESSAGES';
		
		if( !$this ->isLocaleValid( $lang ) )
			return null;
		
		//var_dump( $root, $lang, $category, $domain );
		if( is_null( $root ) )
		{
			if( !defined( 'DIR_LOCALE' ) )
				throw new Exception( 'DIR_LOCALE not defined' );
			$root = DIR_LOCALE;
		}
		
		$domain = (!is_null( $domain )) ? $domain : 'messages';
		
		// render
		$r = sprintf( '%s%s/%s/%s.po', $root, $lang, $category, $domain );
		
		return $r;
	}
	
	public function isLocaleValid( $locale )
	{
		return true; //preg_match( '@([a-z]+_[A_Z]+(\.UTF\-8)?)@', $locale );
		$current = setlocale( LC_COLLATE, "0" );
		$result = setlocale( LC_COLLATE, $locale );
		setlocale( LC_COLLATE, $current );
		//var_dump( $result, $result !== false );
		return $result !== false;
	}
	
	
}
